use anyhow::{Result, anyhow};
use serde_json::Value;
use std::{net::IpAddr, path::Path, str::FromStr};

pub struct ProxyConfig {
    pub in_bound: InBound,
    pub out_bound: OutBound,
}

pub struct InBound {
    pub kind: InBoundKind,
    pub port: u16,
    pub ethan_id: Option<String>,
}

#[derive(Debug, PartialEq)]
pub enum InBoundKind {
    Socks5,
    Ethan,
}

impl FromStr for InBoundKind {
    type Err = anyhow::Error;

    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            "socks5" => Ok(Self::Socks5),
            "ethan" => Ok(Self::Ethan),
            other => Err(anyhow!("not support inbound kind:{}", other)),
        }
    }
}

pub struct OutBound {
    pub kind: OutBoundKind,
    pub port: Option<u16>,
    pub server: Option<IpAddr>,
    pub ethan_id: Option<String>,
}

#[derive(Debug, PartialEq)]
pub enum OutBoundKind {
    Socks5,
    Ethan,
    Freedom,
}
impl FromStr for OutBoundKind {
    type Err = anyhow::Error;

    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            "socks5" => Ok(Self::Socks5),
            "ethan" => Ok(Self::Ethan),
            "freedom" => Ok(Self::Freedom),
            _ => Err(anyhow!("Not support outbound kind")),
        }
    }
}

impl ProxyConfig {
    fn load_from_file(config_path: impl AsRef<Path>) -> Result<Self> {
        let config_path = config_path.as_ref();
        if !config_path.exists() {
            return Err(anyhow!(
                "config file in: {} is not exist",
                config_path.to_string_lossy()
            ));
        }
        let config_content = std::fs::read_to_string(config_path)?;
        ProxyConfig::from_str(&config_content)
    }
}

impl FromStr for ProxyConfig {
    type Err = anyhow::Error;

    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
        let config_json: Value = serde_json::from_str(&s)?;
        let inbound_type = config_json["inbound"]["type"]
            .as_str()
            .expect("inbound type must had value");
        let inbound_port = config_json["inbound"]["port"].as_u64();
        let inbound_ethan_id = match &config_json["inbound"]["ethanId"] {
            Value::String(str) => Some(str.to_string()),
            _ => None,
        };

        let outbound_section = &config_json["outbound"];
        let outbound_type = outbound_section["type"]
            .as_str()
            .expect("outbound type value must had value");

        let inbound_type = InBoundKind::from_str(inbound_type)?;
        let inbound_port = inbound_port
            .map(|x| x as u16)
            .ok_or(anyhow!("value can't be convert to u16"))?;
        let inbound = InBound {
            kind: inbound_type,
            port: inbound_port,
            ethan_id: inbound_ethan_id,
        };

        let outbound = match OutBoundKind::from_str(&outbound_type) {
            Ok(kind) => match kind {
                OutBoundKind::Socks5 | OutBoundKind::Ethan => {
                    let outbound_server = outbound_section["server"]
                        .as_str()
                        .expect("not found field: server at root-outbound");
                    let outbound_port = outbound_section["port"].as_u64();
                    let ethan_id = outbound_section["ethanId"].to_string();
                    let addr = IpAddr::from_str(&outbound_server)?;

                    OutBound {
                        kind,
                        port: outbound_port.map(|x| x as u16),
                        server: Some(addr),
                        ethan_id: Some(ethan_id),
                    }
                }
                OutBoundKind::Freedom => OutBound {
                    kind,
                    port: None,
                    server: None,
                    ethan_id: None,
                },
            },
            Err(err) => {
                log::error!("err when parse sttr to OutBoundKind:{}", err);
                panic!("{}", err);
            }
        };
        Ok(ProxyConfig {
            in_bound: inbound,
            out_bound: outbound,
        })
    }
}

#[cfg(test)]
mod test {
    use anyhow::Result;

    use super::*;

    #[test]
    fn ethan_proxy_config_load_from_str_test() -> Result<()> {
        let config = r#"
       {
            "inbound":{
                "type":"socks5",
                "port":1080
            },
            "outbound":{
                "type":"ethan",
                "server":"127.0.0.1",
                "port":10800,
                "ethanId":""
            }
}"#;
        let config = ProxyConfig::from_str(config)?;
        assert_eq!(config.in_bound.kind, InBoundKind::Socks5);
        assert_eq!(config.in_bound.port, 1080);
        assert_eq!(config.out_bound.kind, OutBoundKind::Ethan);
        assert_eq!(config.out_bound.port, Some(10800));
        assert_eq!(
            config.out_bound.server,
            Some(IpAddr::from_str("127.0.0.1")?)
        );
        Ok(())
    }

    #[test]
    fn ethan_proxy_config_parse_str_test()->Result<()>{
        let config_str=r#"
        {
    "inbound":{
        "type":"ethan",
        "port":10800,
        "ethanId":"abcdefg"
    },
    "outbound":{
        "type":"freedom"
    }
}"#;
        let config=ProxyConfig::from_str(config_str)?;
        assert_eq!(config.in_bound.kind,InBoundKind::Ethan);
        assert_eq!(config.in_bound.port,10800);
        assert_eq!(config.in_bound.ethan_id,Some("abcdefg".to_string()));
        assert_eq!(config.out_bound.kind,OutBoundKind::Freedom);
        Ok(())
    }
}
